Skip to content

Conversation

@onerandomusername
Copy link
Contributor

@onerandomusername onerandomusername commented Oct 21, 2025

Update botstrap to be much more userfriendly. This silences noisy logs that we don't want to see or care about when downloading new server.

We also provide a success message when the entire system is successful.

I've also implemented some small caching in the botcore client to only make requests when necessary, and to only make one request to get all of the webhooks. While yes, webhooks are free, I left request logging enabled so the user could see what requests were made. I didn't see a good reason to silence those, both for transparency and because they already provide logging.

We also provide an invite link if the bot isn't in the guild already, but it is configured.

I've also added support which sets the app's flags to enable message content intents and member intent if they aren't already enabled.

closes #3414

Copy link
Member

@jb3 jb3 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few review comments.

Can we also squash down the history a little bit? I don't think we need 21 commits for the changes here. Obviously keep the constants one separate but I think we can squash down a lot of the other ones.

@onerandomusername onerandomusername force-pushed the qt/beginner-friendly-botstrap branch 5 times, most recently from f5f2c3c to a61eb26 Compare October 25, 2025 21:33
@onerandomusername onerandomusername requested a review from jb3 October 25, 2025 21:37
@onerandomusername
Copy link
Contributor Author

@jb3 I've compressed the history and addressed your comments in the updated history. Comments should now follow pep8, formatted with ruff, and silenced pyright.

Copy link
Member

@jb3 jb3 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing review feedback.

Only a couple of points left now, but also a couple of wider points:

  • I think the with block we use now to run the script is too long and I think things like the webhook management & emoji management should be moved into their own methods that we then call (the with should be as small as possible really).
  • The existing webhook if statement is still a bit scary (I thought I'd left a review comment but can't see/find it now!), I'd prefer creating a new method along the lines of find_matching_webhook, something like:
def find_matching_webhook(existing_webhooks, webhook_model, formatted_name, channel_id):
    for hook in existing_webhooks:
        if hook["id"] == str(webhook_model.id):
            return hook["id"]
        if hook["name"] == formatted_name and hook["channel_id"] == str(channel_id):
            return hook["id"]
    return None

It's all just become a little bit too complex to follow under that context manager and I think that some splitting up into smaller more digestible methods is a good idea here.

botstrap.py Outdated

def get_emoji_contents(self, emoji_id: str | int) -> bytes | None:
"""Fetches the image data for an emoji by ID."""
# emojis are located at https://cdn.discordapp.com/emojis/821611231663751168.png?size=4096
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd use path-params to document this and where that snowflake comes from (even though it's obvious). Also, not sure the size is necessary given we don't then use it for the subsequent request.

Suggested change
# emojis are located at https://cdn.discordapp.com/emojis/821611231663751168.png?size=4096
# Emojis are located at https://cdn.discordapp.com/emojis/{emoji_id}.png

botstrap.py Outdated
Comment on lines 213 to 215
server_channels = self.guild_channels

for channel in server_channels:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
server_channels = self.guild_channels
for channel in server_channels:
for channel in self.guild_channels:

Copy link
Member

@jb3 jb3 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for these comments all being in parts, just keep doing scans and picking up new bits.

Can we also standardise on whether you're using f-strings in this file or using %s interpolation? There is a mix currently (I appreciate it was f-strings before which is not correct, but if you're adding some f-strings and some %s strings we may as well unify).

- provide completion message
- silence httpcore logger
- filter out botcore's warning for patching send_typing
- use short url for contributing link
- only use logging.warning and up for things that need user action
- use logging.fatal when exiting due to error
- use % for all logging statements
@onerandomusername onerandomusername force-pushed the qt/beginner-friendly-botstrap branch from a61eb26 to 57c92b0 Compare October 27, 2025 14:38
this lowers the amount of requests we make and reduces the chance of ratelimiting
* request all webhooks in a single call rather than per configured webhook

* Enable support for multiple identical test bots
sharing the same webhook

This should prevent the limit of 10 webhooks per channel from being reached
without needing to share a specific guild configuration for the PyDis
staff test guild
this makes it easier to configure webhooks:

we now only need to set their ID and not their channel.

Setting the channel ID is only use for botstrap configuration, anyhow.
@onerandomusername onerandomusername force-pushed the qt/beginner-friendly-botstrap branch from 57c92b0 to 82fa0b1 Compare October 27, 2025 19:37
config["emojis"] = self.sync_emojis()

self.write_config_env(config, self.env_file)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should define the dict with all keys instead of adding individually:

e.g.

config = {
  "categories": self.get_categories(),
  ...
}

Comment on lines +397 to +407
if (
# check the existing ID matches the configured one
existing_hook["id"] == str(webhook_model.id)
or (
# check if the name and the channel ID match the configured ones
existing_hook["name"] == formatted_webhook_name
and existing_hook["channel_id"] == str(all_channels[webhook_name])
)
):
webhook_id = existing_hook["id"]
break
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have commented every review this needs changing 🥲.

Split out to a helper method without the complex conditions. Also, comments should always have a capital (and only be present if they actually add something, for simple conditionals they generally don't).

def find_matching_webhook(existing_webhooks, webhook_model, formatted_name, channel_id):
    for hook in existing_webhooks:
        if hook["id"] == str(webhook_model.id):
            return hook["id"]
        if hook["name"] == formatted_name and hook["channel_id"] == str(channel_id):
            return hook["id"]
    return None

response = self.post(
f"/guilds/{self.guild_id}/emojis",
json=payload,
headers={"X-Audit-Log-Reason": f"Creating {new_name} emoji as part of PyDis botstrap"},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
headers={"X-Audit-Log-Reason": f"Creating {new_name} emoji as part of PyDis botstrap"},
headers={"X-Audit-Log-Reason": "Creating emoji as part of PyDis botstrap"},

Comment on lines +270 to +273
payload = {
"name": new_name,
"image": f"data:image/png;base64,{base64.b64encode(emoji_data).decode('utf-8')}",
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cleaner when not done inline:

Suggested change
payload = {
"name": new_name,
"image": f"data:image/png;base64,{base64.b64encode(emoji_data).decode('utf-8')}",
}
image_data = base64.b64encode(emoji_data).decode('utf-8')
payload = {
"name": new_name,
"image": f"data:image/png;base64,{image_data}",
}


def get_emoji_contents(self, id_: str | int) -> bytes | None:
"""Fetches the image data for an emoji by ID."""
# emojis are located at https://cdn.discordapp.com/emojis/{emoji_id}.{ext}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# emojis are located at https://cdn.discordapp.com/emojis/{emoji_id}.{ext}
# Emojis are located at https://cdn.discordapp.com/emojis/{emoji_id}.{ext}

Comment on lines +467 to +468
with botstrap:
botstrap.run()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this is the way this should be done or whether it'd be cleaner to create a DiscordClient here, pass it into the botstrap initialiser and then avoid having to define the __exit__ method for the BotStrapper class.

e.g.

with DiscordClient(guild_id=GUILD_ID) as discord_client:
    botstrap = BotStrapper(client=discord_client)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

More user friendly botstrap

2 participants